35. Projekt końcowy - tydzień 2 z 3
Wyzwania:
- poznasz metody zapisywania danych sesji użytkownika,
- dowiesz się jak obsługiwać ciastka oraz
localStorage.
35.1. Zapisywanie danych pomiędzy sesjami
Kiedy zalogujesz się do swojego panelu kursanta, możesz odświeżyć stronę i nadal będziesz widzieć swój panel. Jednak skąd właściwie serwer wiedział, że ma wyświetlić właśnie Twoje dane? Skąd wiedział, że wcześniej udało Ci się poprawnie zalogować?
To jedna z podstawowych funkcjonalności witryn internetowych, bez której korzystanie ze stron byłoby dużo trudniejsze! W tym module dowiesz się, w jaki sposób zaimplementować tę samą funkcjonalność w swoim projekcie.
Możesz wykorzystać te informacje nie tylko do zapamiętywania czy użytkownik jest zalogowany, ale również do przechowywania innych informacji. W poprzednim module zasugerowaliśmy na przykład, że warto by było zapamiętywać koszyk użytkownika. Dzięki temu po odświeżeniu strony albo ponownym uruchomieniu komputera, dane zawarte w koszyku już by nie przepadały!
Zanim jednak nauczymy Cię jak korzystać z sesji, ciastek i localStorage, musimy zająć się kwestią prawną...
Ostrzeżenie o ciastkach
Unijna ustawa GDPR wymaga, aby informować użytkowników o zapisywaniu danych w ich przeglądarce – dotyczy to zarówno cookies, jak i localStorag czy sessionStorage. Oznacza to, że jeśli używasz np. Google Analytics do badania ruchu na stronie, to musisz informować użytkowników strony o używaniu ciastek (bądź innym zapisywaniu danych). Właśnie dlatego na prawie wszystkich stronach możesz znaleźć jakąś formę informacji o ciastkach.
Pamiętaj, że nie wystarczy poinformować użytkownika, ale trzeba uzyskać od niego zgodę! Dopiero wtedy można używać cookies.
Istnieją jednak istotne wyjątki od tej zasady, spośród których nas najbardziej interesuje wykorzystanie cookies niezbędnych do funkcjonowania strony. Oznacza to, że bez zgody użytkownika nie możemy wykorzystywać ciastek np. do tego, aby polecać inne produkty osobom, które już kiedyś były na naszej stronie. Możemy jednak używać ciastek, aby użytkownik pozostawał zalogowany po odświeżeniu strony. Zwróć uwagę, że w tych przykładach samo ciastko mogłoby być identyczne – liczy się sposób jego wykorzystania!
Podsumowując, nie musimy martwić się zgodą użytkownika w przykładzie, o którym mówimy. Pamiętaj jednak, aby zawsze ostrożnie podchodzić do korzystania z jakichkolwiek rozwiązań rozpoznających użytkowników, badających ruch na stronie, wyświetlających reklamy, etc.
Możemy w takim razie przejść do implementacji rozwiązania, które pozwoli nam na zapamiętanie koszyka przy odświeżeniu strony.
35.2. Cookies i localStorage
Zacznijmy od zastosowania najbardziej podstawowego rozwiązania, czyli zachowania zawartości koszyka w cookies. Nie jest to idealne rozwiązanie, ale będzie dla nas świetnym punktem wyjścia.
W tej sytuacji backend w ogóle nie jest angażowany – wszystko odbywa się na frontendzie.
Ciastka są przechowywane we właściwości document.cookies. Aby podejrzeć, jak wygląda ta właściwość, na dowolnej stronie (np. tutaj) możesz w konsoli narzędzi developerskich wpisać:
console.log(document.cookie);
Jak widzisz, jest to jeden ciąg znaków. Poszczególne ciastka są w nim oddzielone średnikami. Każde z nich zawiera nazwę, znak równości oraz wartość. To wszystko, do czego mamy dostęp z poziomu JS-a.
Przeglądarka ma jednak dużo więcej informacji o każdym ciastku – możesz je zobaczyć, przechodząc w narzędziach developerskich do zakładki Application. Tam w lewej kolumnie znajdziesz pozycję Cookies, pod którą znajdzie się co najmniej jedna domena. Dla każdej z domen będzie osobny zestaw ciastek.
Jak w takim razie możemy zapisywać coś w ciastku? Warto do tego używać funkcji pomocniczych, które mogą wyglądać na przykład tak:
function getCookie(name) {
var v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
return v ? v[2] : null;
}
function setCookie(name, value, days) {
var d = new Date;
d.setTime(d.getTime() + 24*60*60*1000*days);
document.cookie = name + "=" + value + ";path=/;expires=" + d.toGMTString();
}
function deleteCookie(name) { setCookie(name, '', -1); }
Zwróć uwagę, że w przypadku zapisywania ciastka podajemy liczbę dni – będzie ona użyta do obliczenia daty ważności ciastka. Po tej dacie ciastko ulegnie samozniszczeniu.
Lepsze rozwiązanie: localStorage
Cookies stanowią bardzo starą technologię webową i – jak już zapewne się domyślasz – przechowywanie w nich większych ilości informacji nie byłoby dobrym pomysłem. Posiadają one limit rozmiaru, a ich odczytywanie nie jest zoptymalizowane.
Mamy jednak do dyspozycji znacznie lepsze rozwiązanie! Jest to localStorage oraz sessionStorage. Różnią się między sobą tylko tym, że pierwsze pozwala na zapisanie danych na dłużej, a sessionStorage jest czyszczony w momencie zamknięcia okna lub karty przeglądarki – dlatego skupimy się na localStorage.
Korzystanie z localStorage jest banalnie proste, ponieważ przeglądarki oferują klasę o tej nazwie. Wystarczy skorzystać z jej metod:
localStorage.setItem('login', 'Kodilla');
const login = localStorage.getItem('login');
console.log(login); // Kodilla
localStorage.removeItem('login');
Oczywiście, w localStorage możemy też przechowywać JSON, co pozwoli nam na zapisywanie i odczytywanie danych o bardziej złożonej strukturze:
const cart = {
products: [
{id: 123, count: 3, notes: 'red shirt with black print'},
]
};
localStorage.setItem('cart', JSON.stringify(cart));
const cartProducts = JSON.parse(localStorage.getItem('cart')).products;
console.log(cartProducts[0].count); // 3
localStorage.removeItem('cart');
Wykorzystanie w React-Redux
Kiedy znasz już sposoby zapisywania informacji w pamięci przeglądarki, możesz zastanowić się, w jaki sposób wykorzystać ten mechanizm do zapisywania koszyka użytkownika.
Jednym z rozwiązań może być wykorzystanie Redux-Thunk – do tej pory w thunkach wykonywaliśmy połączenia AJAX-owe z API, ale nic nie stoi na przeszkodzie, aby informacje dotyczące koszyka wysyłać do localStorage zamiast do backendu. Dzięki temu niekomponenty nie będą musiały w ogóle wiedzieć o tej operacji, a cały kod odpowiedzialny za zapisywanie koszyka w pamięci przeglądarki będzie zlokalizowany w jednym miejscu.
35.3. Sesje w Expressie
Jak wspomnieliśmy powyżej, localStorage pozwoli nam na zapisanie danych w pamięci przeglądarki użytkownika. Dotyczy to jednak tylko danej przeglądarki na danym komputerze.
Gdybyśmy np. dodali do naszego projektu możliwość zalogowania się, to ten sam użytkownik miałby inny koszyk na swoim komputerze, niż na smartfonie. Kolejnym problemem może być to, że nasz backend nie ma żadnej informacji o tym koszyku – a dla właściciela sklepu może być niezmiernie istotne to, ile osób dodaje produkty do koszyka, a potem rezygnuje z zakupu. Może nawet chciałby, żeby już w koszyku wpisywało się swój adres mailowy, aby później (za zgodą użytkownika) wysłać mu przypomnienie o niezłożonym zamówieniu?
W tych sytuacjach idealnym rozwiązaniem byłoby zapisywanie w cookies tylko jakiegoś identyfikatora użytkownika – ale już same informacje dotyczące jego koszyka byłyby wysyłane AJAX-em do backendu. Całe szczęście, wiele osób przed nami wpadło na ten pomysł!
Skupmy się na drugim przykładzie, czyli zapisywaniu koszyków w backendzie dla naszej informacji. Wykorzystamy do tego pakiet express-session, z którym spotkaliśmy się już przy okazji poznawania tematu autoryzacji Google z użyciem Passportu.
Wystarczy, że zainstalujemy ten pakiet, a następnie w pliku z kodem expressowego serwera dodamy:
const session = require('express-session');
app.use(session({secret: 'shhh!'}));
Jak zapewne pamiętasz, parametr secret służy do jej bardziej unikalnego kodowania, ponieważ jest wykorzystywany przy generowaniu i odczytywaniu informacji o sesji. Jego nieprzewidywalność (sami decydujemy o treści) powoduje, że inne podmioty niż nasz serwer mają duży problem z jej odkodowaniem. W związku z tym pamiętaj, aby zmienić jego wartość na dowolną inną, najlepiej trudną do odgadnięcia.
Teraz w endpointach możesz zacząć korzystać z właściwości req.session – traktuj go jak zwyczajny obiekt, czyli zapisuj do niego informacje np. za pomocą:
req.session.login = 'Kodilla';
req.session.cart = {
products: [
{id: 123, count: 3, notes: 'red shirt with black print'},
]
};
console.log(req.session.login); // Kodilla
console.log(req.session.cart.products.id); // 123
Dzięki korzystaniu z obiektu req.session, informacje w nim zapisane będą dla nas dostępne przy każdym połączeniu tej przeglądarki. Dlaczego? Możesz to sprawdzić samodzielnie, zaglądając do ciastek strony, która korzysta z tego kodu. Znajdziesz tam ciastko, które będzie miało (najprawdopodobniej) nazwę connect.sid. Jego wartością jest losowy, unikalny identyfikator sesji. To właśnie dzięki temu ciastku Express wie, która sesja należy do tego użytkownika.
Jak wykorzystać sesję?
Możesz się teraz zastanawiać co najlepiej przechowywać w sesji. Oczywiście, mogłyby to być wszystkie informacje dotyczące koszyka, ale to nie zawsze jest najwygodniejsze. Dużo lepszym rozwiązaniem będzie dodanie do modelu zamówienia informacji o jego statusie – dzięki temu będziemy mogli zapisywać "zamówienie, które nie jest jeszcze złożone", czyli właśnie koszyk. W momencie złożenia zamówienia wystarczy, że ten rekord zaktualizujemy i w ten sposób "zamówienie niezłożone" (czyli koszyk) stanie się złożonym zamówieniem.
W takim razie wszystko, co jest nam potrzebne, to generowanie unikalnego identyfikatora koszyka. W sesji możemy wtedy przechowywać tylko informację o identyfikatorze koszyka, a wszystkie dane koszyka w kolekcji zamówień w MongoDB. Oczywiście, będzie to również wymagało stworzenia endpointów do odczytu (przy otwarciu strony) oraz zapisu (przy każdej zmianie) stanu koszyka.
Zapisywanie sesji
Domyślnie, sesje Expressa są zapisywane w pamięci procesu serwera. Oznacza to, że kiedy tylko zresetujemy nasz serwer, wszystkie zapisane sesje przepadną. W niektórych sytuacjach może to być dobre rozwiązanie, ale z pewnością nie w omawianym przez nas przykładzie! Dlatego pozostaje nam jeszcze skorzystanie z jednego z wielu rozwiązań dla express-session, które pozwalają na trwałe zapisywanie sesji. Biorąc pod uwagę nasz stack technologiczny, najłatwiej będzie nam zapisywać sesje w MongoDB.
Jeśli w pliku serwera mamy już wykorzystany Mongoose, to wystarczy drobna zmiana! Załóżmy, że tak wygląda fragment Twojego pliku:
// ...
const session = require('express-session');
mongoose.connect('mongodb://localhost:27017/companyDB', { useNewUrlParser: true, useUnifiedTopology: true });
const db = mongoose.connection;
app.use(session({
secret: 'shhh!',
}));
// ...
Teraz wystarczy zainstalować pakiet connect-mongo i wprowadzić następujące zmiany w powyższym fragmencie:
// ...
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
mongoose.connect('mongodb://localhost:27017/companyDB', { useNewUrlParser: true, useUnifiedTopology: true });
const db = mongoose.connection;
app.use(session({
secret: 'shhh!',
store: new MongoStore({ mongooseConnection: db }),
}));
// ...
Wystarczyło dodać import (MongoStore) oraz drugą opcję przy inicjowaniu sesji (store). Możemy śmiało wykorzystać tę samą bazę – dodana zostanie nowa kolekcja z sesjami o nazwie sessions.
I to wszystko – korzystanie z sesji wygląda tak samo, jak wcześniej. Musieliśmy jedynie zmienić konfigurację express-session.